浅淡requestAnimationFrame |
您所在的位置:网站首页 › requestanimationframe this › 浅淡requestAnimationFrame |
requestAnimationFrame
在性能优化上,最近研究了一下这个接口 MDN是这么描述的: window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行 说实话,上面描述的不是很清除,对初学者来说,容易混淆 接下来我一步一步拆解,带你理解这个接口的作用 我们先来看看什么是重绘 重绘(Repaint)在浏览器中,重绘指的是改变样式不影响它在文档流中的位置,例如: Hello World let app = document.querySelector('#app') app.style.backgroundColor = "#ddd" app.style.color = "blue" 复制代码显然,因为我上面的操作不会改变span的在文档中的位置 所以浏览器仅仅需要重绘第一个span所在位置的像素,完全不影响其他地方 因为不影响其他元素的布局,所以不需要重绘它们 重排(回流)(ReFlow)重排意味着,因为某个元素的布局改变,导致其他元素的位置改变,所以其他元素也要重新绘制 还是如上的例子,当我尝试改变内边距: app.style.padding = "10px" 复制代码显然第二个盒子被往右推了: 上面是最简单的例子,虽然看起来并无影响 但我们可以试想,如果应该一个元素的小小变化,而引起整个页面重排,性能消耗是极大的 上面的例子还可以看出,重排肯定要重绘,但是重绘不一定要重排 动画的痛点?Css的动画与过渡在页面中经常使用,也常常伴随着大量的重绘与重排 我们知道,动画的标准频率是60hz,这意味着,要想做出流畅的动画,需要每16.6ms渲染一帧,也就是在1000ms内渲染60次 虽然渲染过程是由浏览器完成的,但是我们可以通过编码,让元素属性在合适的时间内改变,从而让浏览器在16.6ms内完成单次渲染 动画为什么会卡? 理想总是很丰满,但现实很残酷。 我们知道,在同一个渲染进程中,存在JS线程和GUI线程,JS线程负责执行JS代码,GUI线程负责渲染页面(这里是渲染而不是绘制,绘制由浏览器进程完成)。 因为JS是可以操作DOM的,如果JS线程和GUI线程同时执行,那么渲染线程前后获得的元素可能就不一致了 所以JS执行和页面渲染是互斥的,如果JS执行时间过长,就会导致页面动画卡顿。 试想一下,假如我用js改变某一个DOM元素的高度,我希望每100ms增加1px let count = 100 setInterval(() => { app.style.height = (count++) + 'px' },100) 复制代码有两个问题产生: JS有可能长时间占据主线程(还有其它事要做),假设JS一直占据500ms,那么就意味着JS操作元素增加10px才渲染新的一帧,那么页面动画看起来就卡卡的,感觉元素在瞬间移动,而不是慢慢过渡 定时器不准确,导致JS没有在合适的时间内改变元素属性第一个问题可以通过Web Workers解决,把无关Dom操作的耗时操作放到Web Workers中 第二个问题,就可以通过requestAnimationFrame解决 requestAnimationRrame比定时器精确? 并不是,解决第二个问题通过提高精度是不靠谱的,最好的方法就是在浏览器重新绘制新的一帧之前,执行一些必要的操作,比如这个函数 我们向requestAnimationRrame传入回调函数,这样浏览器,每次绘制之前必然会执行我们js改变元素的代码,动画也就流畅了 当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 requestAnimationFrame((temp) => { console.log(temp) //temp是从第一次执行开始的时间戳 }) 复制代码但是requestAnimationFrame只会执行一次,我们需要递归调用 let ref = requestAnimationFrame(test) function test(temp) { console.log(temp) ref = requestAnimationFrame(test) } 复制代码你可能还是很迷惑执行的过程,上面不是递归吗?没有出口的话,浏览器不是会一下子卡死吗? 但结果却不是这样: 只要记住:requestAnimationFrame()里面的回调函数不是立即执行,而是绘制的前一时刻由浏览器调用执行,因为实际上requestAnimationFrame是一个宏任务。此外次API专属于浏览器,不能在NodeJs环境运行(Node没有GUI,也就不会有此API)。 和定时器一样,因为调用完了就销毁,所以只调用一次requestAnimationFrame就只执行一次 所以该函数一般来说最多每秒执行60次(如果屏幕刷新率是60HZ) 利用requestAnimationFrame思路: let startTime = 0 let count = 100 let ref = requestAnimationFrame(test) function test(temp) { if(temp - startTime > 100) { app.style.height = (count++) + 'px' time = startTime } ref = requestAnimationFrame(test) } 复制代码 temp和reftemp 回调函数内可以接收参数,也就是我上文称为的temp,一旦执行,它从0ms开始增加计时 且多个requestAnimationFrame的temp值的一样的,例如: requestAnimationFrame(one) function one(temp) { console.log(temp,'one') requestAnimationFrame(one) } setTimeout(() => { requestAnimationFrame(two) function two(temp) { console.log(temp,'two') e = requestAnimationFrame(two) } },4000) 复制代码就算用定时器延迟执行,temp值依然是一样的: ref requestAnimationFrame()的返回值(上文我称为ref),也是有作用的,代表requestAnimationFrame回调执行的次数: let ref = requestAnimationFrame(one) function one() { console.log(ref) ref = requestAnimationFrame(one) } 复制代码 停止执行你可能注意到,上面所有关于requestAnimationFrame的写法都是没有出口的,意味着会一直执行 我们可以cancelAnimationFrame()取消回调函数 const beginBtn = document.querySelector("#begin") const endBtn = document.querySelector("#end") const app = document.querySelector("#app") let ref beginBtn.onclick = () => { ref = requestAnimationFrame(one) } endBtn.onclick = () => { cancelAnimationFrame(ref) } let startTime = 0 function one(temp) { if(temp - startTime > 100) { app.style.height = (count++) + 'px' time = startTime } ref = requestAnimationFrame(one) } 复制代码 setInterval和setTimoutsetInterval和setTimout不够精确的原因是内在机制决定。当计时结束不意味着回调函数会立即执行 而是将回调函数放到回调队列末尾,导致实际执行时间总是大于预定的时间,例如下面简单的测试: console.time() setTimeout(() => { console.timeEnd() },1000) 复制代码测试两次: 结果都将大于1000ms,而且时间不固定(注:3036和3048是定时器的返回值) requestAnimationFrame 采用 浏览器时间间隔 ,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,消耗性能;也不会因为间隔时间太长,使用动画卡顿不流畅 其他MDN范例: const element = document.getElementById('some-element-you-want-to-animate'); let start; function step(timestamp) { if (start === undefined) { start = timestamp; } const elapsed = timestamp - start; //这里使用`Math.min()`确保元素刚好停在200px的位置。 element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)'; if (elapsed < 2000) { // 在两秒后停止动画 window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); 复制代码上面的例子说明,如果想在回调里面控制执行时刻,推荐使用回调参数timestamp作为时间的参考 此外,也可以结合new Date()来控制时间 兼容性: |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |